#include "batman.hpp"

std::string dstr(enum direction d) {
	switch (d) {
		case N:
			return "North";
		case S:
			return "South";
		case E:
			return "East";
		case W:
			return "West";
	}
    return ""; // To suppress a compiler warning
}

BATman::BATman() {
	for (int i = 0; i < NUM_DIRS; i++) {
		pthread_mutex_init (&Mutex[i], NULL);
		pthread_cond_init (&Wait[i], NULL);
		IsWaiting[i] = false;
        IsFirstWaiting[i] = false;
	}

	pthread_mutex_init (&CondMutex, NULL);
	pthread_mutex_init (&crossing, NULL);
	pthread_mutex_init (&FWMutex, NULL);
}

BATman::~BATman() {
	for (int i = 0; i < NUM_DIRS; i++) {
		pthread_mutex_destroy (&Mutex[i]);
		pthread_cond_destroy (&Wait[i]);
	}
	
	pthread_mutex_destroy (&crossing);
	pthread_mutex_destroy (&FWMutex);
	pthread_mutex_destroy (&CondMutex);
}

/* Queue up the BAT */
void BATman::arrive(BAT *b) {
    std::ostringstream s;
    s << "BAT " << b->num() << " from " << dstr(b->dir()) << " arrives at crossing\n";
    std::cout << s.str();
	
	pthread_mutex_lock (&Mutex[b->dir()]);
	Queue[b->dir()].push (b);
	pthread_mutex_unlock (&Mutex[b->dir()]);
}

void BATman::cross(BAT *b) {
	if (Queue[RIGHT_QUEUE(b)].size() > 0 || Queue[b->dir()].front() != b) {
		pthread_mutex_lock (&Mutex[b->dir()]);
		IsWaiting[b->dir()] = true;
        pthread_mutex_unlock (&Mutex[b->dir()]);
		while (true) {
			if (Queue[b->dir()].front() == b) {
				pthread_mutex_lock (&FWMutex);
                IsFirstWaiting[b->dir()] = true;
				pthread_mutex_unlock (&FWMutex);
			}

			pthread_mutex_lock (&CondMutex);
			pthread_cond_wait (&Wait[b->dir()], &CondMutex);
			pthread_mutex_unlock (&CondMutex);

            if (Queue[b->dir()].front() != b) {
				pthread_mutex_lock (&CondMutex);
				pthread_cond_signal (&Wait[b->dir()]);
				pthread_mutex_unlock (&CondMutex);
			} else {
				break;
			}
		}
	}

	pthread_mutex_lock (&FWMutex);
	IsFirstWaiting[b->dir()] = false;
	pthread_mutex_unlock (&FWMutex);

	pthread_mutex_lock (&crossing);
    sleep(1); // It takes one second for a BAT to cross
	pthread_mutex_unlock (&crossing);
}

/* Dequeue the bat and signal the BAT to the left to go, if it's waiting. */
void BATman::leave(BAT *b) {
    std::ostringstream s;
    s << "BAT " << b->num() << " from " << dstr(b->dir()) << " leaving crossing\n";
    std::cout << s.str();

	if (IsWaiting[LEFT_QUEUE(b)]) {
		pthread_mutex_lock (&CondMutex);
		pthread_cond_signal (&Wait[LEFT_QUEUE(b)]);
		pthread_mutex_unlock (&CondMutex);
	}

	pthread_mutex_lock (&Mutex[b->dir()]);
	Queue[b->dir()].pop();
	if (Queue[b->dir()].size() == 0) {
		IsWaiting[b->dir()] = false;
	} else {
		pthread_mutex_lock (&CondMutex);
		pthread_cond_signal (&Wait[b->dir()]);
		pthread_mutex_unlock (&CondMutex);
	}
	pthread_mutex_unlock (&Mutex[b->dir()]);
}

/* Thread code for the batman (why so cereal).  Loops continuously looking for
 * deadlocks.  If one is detected (i.e. all BATs are waiting), then the manager
 * breaks the deadlock by signaling the north BAT to go.
 */
void BATman::check() {
	while (true) {
		bool deadlock = true;

		pthread_mutex_lock (&FWMutex);
        for (unsigned int i=0; i < NUM_DIRS; i++) {
			if (!IsFirstWaiting[i]) {
				deadlock = false;
			}
		}
		pthread_mutex_unlock (&FWMutex);

		if (deadlock) {
            std::cout << "Deadlock Detected; signaling North to go.\n"; // I refuse to say "BAT jam detected"
			pthread_mutex_lock (&CondMutex);
			pthread_cond_signal (&Wait[0]);
			pthread_mutex_unlock (&CondMutex);
		}

		sleep (1);
	}
}

BAT::BAT(int _num, enum direction _dir, BATman *_man) 
  : m_num(_num), m_dir(_dir), m_man(_man) {}

// Thread code for a BAT
void BAT::go() {
	m_man->arrive (this);
	sleep (1);
    m_man->cross (this);
	m_man->leave (this);
}

// Wrapper function for the manager that's able to be called by the pthread library.
void * BATmanGo(void *arg) {
	BATman *man = (BATman*)arg;
	man->check();
	return NULL;
}

// Wrapper function for a bat that's able to be called by the pthread library.
void * BATGo(void *arg) {
	BAT *b = (BAT*)arg;
	b->go();
	return NULL;
}

int main(int argc, char **argv) {
	BATman manager;
	int num = 0;
    std::vector<BAT*> bats;
    std::vector<pthread_t> thr;
	pthread_t manager_thr;

	if (argc < 2) {
		std::cerr << "Error: Usage " << argv[0] << " <n,s,e,w>" << std::endl;
		return 1;
	}

	// Spawn manager running check()
	pthread_create(&manager_thr, NULL, &BATmanGo, (void *)(&manager));

	// Parse directions string
	for (unsigned int i=0; i < strlen(argv[1]); i++) {
        bool invalid;
		enum direction dir;

		switch (argv[1][i]) {
        case 'n':
        case 'N':
            dir = N;
            break;
        case 's':
        case 'S':
            dir = S;
            break;
        case 'e':
        case 'E':
            dir = E;
            break;
        case 'w':
        case 'W':
            dir = W;
            break;
        default:
            std::ostringstream s; s << "Error: Invalid direction " << argv[1][i] << std::endl;
            std::cerr << s.str();
            invalid = false;
		}

        if (invalid) continue; // Don't spawn threads for invalid input

		// Spawn threads
        BAT *b = new BAT(num++, dir, &manager);
        thr.push_back(0);  // Create the pthread_t entry for the thread to modify
		pthread_create(&thr.at(i), NULL, &BATGo, (void *) b);
	}

    std::vector<pthread_t>::iterator vi, ve;
    for (vi = thr.begin(), ve = thr.end(); vi != ve; ++vi) {
		pthread_join(*vi, NULL);
	}

	pthread_cancel(manager_thr);
	pthread_join(manager_thr, NULL);

    return 0;
}

